﻿using System;
using System.Threading;

namespace Sbcx.Threading {

    /// <summary>
    /// Just like <see cref="Lazy<T>"/>, but mutable
    /// </summary>
    /// <remarks>The factory Func contains an action to reset</remarks>
    /// <typeparam name="T">value returned from the factory</typeparam>
    public sealed class MutableLazy<T> {

        readonly Func<Action, T> _factory;
        readonly ManualResetEventSlim _haveValue = new ManualResetEventSlim (false);
        readonly AutoResetEvent _autoResetEvent = new AutoResetEvent (true);
        T _value;
        CancellationToken _cancellationToken;

        /// <summary>
        /// Initializes a new instance of the MutableLazy class.
        /// </summary>
        /// <param name="factory">invoking action resets the mutable lazy, factory returns new instance of T</param>
        public MutableLazy (Func<Action, T> factory)
            : this (factory, CancellationToken.None) {
        }

        /// <summary>
        /// Initializes a new instance of the MutableLazy class.
        /// </summary>
        /// <param name="factory">invoking action resets the mutable lazy, factory returns new instance of T</param>
        /// <param name="cancellationToken">token to wait on during Value block</param>
        public MutableLazy (
            Func<Action, T> factory, 
            CancellationToken cancellationToken) {

            _factory = factory;
            _cancellationToken = cancellationToken;
        }

        /// <summary>
        /// Returns the current value, or, blocks, 
        /// invokes the factory, and returns the value
        /// </summary>
        public T Value {
            get {
                if (!_haveValue.IsSet)
                    // we don't currently have a value
                    if (_autoResetEvent.WaitOne (100)) {
                        // only want one thread to invoke the factory
                        try {
                            _value = _factory (Reset);
                        }
                        catch {
                            // make sure the factory doesn't throw
                            _value = default (T);
                            throw;
                        }
                        finally {
                            // if the factory throws we need to set to avoid
                            // an potential infinite wait
                            _haveValue.Set ();
                        }
                    }
                    else
                        // wait for 1st thread to set
                        _haveValue.Wait (_cancellationToken);

                // returns the value from the factory
                return _value;
            }
        }

        void Reset () {
            if (!_haveValue.IsSet)
                return;
            _autoResetEvent.Set ();
            _haveValue.Reset ();
        }
    }
}